package httputil.okhttpinterceptor;

import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import java.security.cert.Certificate;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * An SSLHostnameVerifier checks whether a hostname is acceptable during SSL
 * authentication.  By default, {@link #COMMON} is used, allowing any level
 * of subdomains for SSL certificates with wildcard domains.  If only one
 * level of subdomains is allowed for SSL certificates with wildcard domains,
 * use {@link #STRICT}.
 */
public interface SSLHostnameVerifier {
    /**
     * The ANY SSLHostnameVerifier allows any hostname, which can be useful
     * during initial development when a valid SSL certificate is not available
     * but is not recommended for production because it would permit an invalid
     * SSL certificate.
     */
    final static public Builtin ANY    = new Builtin("ANY");
    /**
     * The COMMON SSLHostnameVerifier applies common rules for checking
     * hostnames during SSL authentication (similar to
     * org.apache.http.conn.ssl.BrowserCompatHostnameVerifier). It allows any
     * level of subdomains for SSL certificates with wildcard domains.
     */
    final static public Builtin COMMON = new Builtin("COMMON");
    /**
     * The STRICT SSLHostnameVerifier applies strict rules
     * for checking hostnames during SSL authentication (similar to
     * org.apache.http.conn.ssl.StrictHostnameVerifier).  It allows one level
     * of subdomain for SSL certificates with wildcard domains like RFC 2818.
     */
    final static public Builtin STRICT = new Builtin("STRICT");

    /**
     * Checks during SSL authentication that a hostname matches the Common Name
     * or "DNS" Subject alts from the SSL certificate presented by the server.
     * @param hostname	the DNS host name of the server
     * @param cns	common names from the SSL certificate presented by the server
     * @param subjectAlts	alternative subject names from the SSL certificate presented by the server
     * @throws SSLException if the hostname isn't acceptable
     */
    public void verify(String hostname, String[] cns, String[] subjectAlts) throws SSLException;

    /**
     * HostnameVerifierAdapter verifies the hostname,SSLSession and X509Certificate certificate.
     * */
    static class HostnameVerifierAdapter implements HostnameVerifier {
        private SSLHostnameVerifier verifier;

        public HostnameVerifierAdapter(SSLHostnameVerifier verifier) {
            this.verifier = verifier;
        }
        /**
         * verify method verifies the incoming hostname and SSLSession.
         * @param hostname denotes the hostname.
         * @param session represents the SSLSession containing the pper certificates.
         * @return true if the hostname and peer certificates are valid and false if they are invalid.
         * */
        @Override
        public boolean verify(String hostname, SSLSession session) {
            try {
                Certificate[] certificates = session.getPeerCertificates();
                verify(hostname, (X509Certificate) certificates[0]);
                return true;
            } catch(SSLException e) {
                return false;
            }
        }
        /**
         * verify method verifies the hostname and X509Certificate certificate.
         * @param hostname denotes the hostname.
         * @param cert represents the X509Certificate certificate.
         * @throws SSLException if the hostname or certificate is/are invalid.
         * */
        public void verify(String hostname, X509Certificate cert) throws SSLException {
            ArrayList<String> cnArray = new ArrayList<>();
            try {
                LdapName ldapDN = new LdapName(cert.getSubjectX500Principal().getName());
                for(Rdn rdn: ldapDN.getRdns()) {
                    Object value = rdn.getValue();
                    if ( "CN".equalsIgnoreCase(rdn.getType()) && value instanceof String ) {
                        cnArray.add((String) value);
                    }
                }
                int type_dnsName = 2;
                int type_ipAddress = 7;
                ArrayList<String> subjectAltArray = new ArrayList<>();
                Collection<List<?>> alts = cert.getSubjectAlternativeNames();
                if ( alts != null ) {
                    for ( List<?> alt : alts ) {
                        if ( alt != null && alt.size() == 2 && alt.get(1) instanceof String ) {
                            Integer type = (Integer) alt.get(0);
                            if ( type == type_dnsName || type == type_ipAddress ) {
                                subjectAltArray.add((String) alt.get(1));
                            }
                        }
                    }
                }
                String[] cns = cnArray.toArray(new String[cnArray.size()]);
                String[] subjectAlts = subjectAltArray.toArray(new String[subjectAltArray.size()]);
                verifier.verify(hostname, cns, subjectAlts);
            }
            catch(CertificateParsingException e) {
                throw new RuntimeException(e);
            }
            catch(InvalidNameException e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * Builtin supports builtin implementations of SSLHostnameVerifier.
     */
    public class Builtin implements SSLHostnameVerifier {
        private String name;
        private Builtin(String name) {
            super();
            this.name = name;
        }
        @Override
        public void verify(String hostname, String[] cns, String[] subjectAlts) throws SSLException {
            throw new RuntimeException(
                    "SSLHostnameVerifier.Builtin called directly instead of passed as parameter");
        }
        /**
         * Returns the name of the built-in.
         * @return	the built-in name
         */
        public String getName() {
            return name;
        }

    }
}
